﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Windows.Forms;
using System.Diagnostics;
using System.Drawing;
using System.Collections.Generic;

/*
SQL File Layout Viewer: abstract utility to visualize the layout of 
data in SQL Server files
Copyright (C) 2013  Merrill Aldrich

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

namespace SqlFileLayoutViewer {
    public partial class MainForm : Form {

        public MainForm() {
            InitializeComponent();

            // Populate dictionary of page type labels

            pageTypeLabels.Add(1, "Data / Clustered Index Leaf");
            pageTypeLabels.Add(2, "Index");
            pageTypeLabels.Add(3, "Mixed LOB");
            pageTypeLabels.Add(4, "Dedicated LOB");
            pageTypeLabels.Add(5, "Unknown/Unused");
            pageTypeLabels.Add(6, "Unknown/Unused");
            pageTypeLabels.Add(7, "Sort");
            pageTypeLabels.Add(8, "GAM Global Alloc Map");
            pageTypeLabels.Add(9, "SGAM Shared Alloc Map");
            pageTypeLabels.Add(10, "IAM Index Alloc Map");
            pageTypeLabels.Add(11, "PFS Page Free Space");
            pageTypeLabels.Add(12, "Unknown/Unused");
            pageTypeLabels.Add(13, "Database Boot");
            pageTypeLabels.Add(14, "Unknown/Unused");
            pageTypeLabels.Add(15, "File Header");
            pageTypeLabels.Add(16, "Diff Map");
            pageTypeLabels.Add(17, "ML Minimal Log Map");

            // Gather all the page details textboxes into a collection
            pageDetailControls.Add(SchemaText);
            pageDetailControls.Add(ObjectText);
            pageDetailControls.Add(IndexText);
            pageDetailControls.Add(PartitionText);

            pageDetailControls.Add(FileIDText);
            pageDetailControls.Add(PageIDText);
            pageDetailControls.Add(PageTypeText);
            pageDetailControls.Add(PrevFileText);
            pageDetailControls.Add(PrevPageText);
            pageDetailControls.Add(NextFileText);
            pageDetailControls.Add(NextPageText);
            pageDetailControls.Add(ObjectIDText);
            pageDetailControls.Add(IndexIDText);
            pageDetailControls.Add(PartitionIDText);
            pageDetailControls.Add(FragText);
            pageDetailControls.Add(FreeSpaceText);

        }

        #region Properties

        // Object that analyses the database file structure
        private DBAnalyzer dba;

        // Object that paints the bitmap from the database page analysis
        private PageBitmap pbm;

        // "Magnifying glass" UI element that enlarges areas of the bitmap
        private Magnifier mag;

        // Collection of controls that display DB page details
        private List<TextBox> pageDetailControls = new List<TextBox>();

        // Dictionary of page type descriptive labels
        private Dictionary<int, string> pageTypeLabels = new Dictionary<int, string>();

        // Display preferences
        private bool highlightFragPref = true;
        private bool showPageFreeSpacePref = true;

        // Mouse position to cross-reference DB page and bitmap image
        private Point mouseClickLocation;

        #endregion

        #region GUI functions

        public void ShowChooseDatabase() {

            // Ask the user to select a database, settings

            ChooseDatabase dlg = new ChooseDatabase();
            dlg.Owner = this;

            // If there is a previous analysis, autofill with the last 
            // analysis' server and database names

            if (dba != null) {
                dlg.ServerComboBox.Text = dba.SQLServerName;
                dlg.DatabaseComboBox.Text = dba.DatabaseName;
                dlg.EnableAnalyzeBtn();
            }

            // Bring over last used settings for display prefs from this form
            dlg.highlightFragCheckbox.Checked = this.highlightFragToolStripMenuItem.Checked;
            dlg.showPageFreeSpaceCheckbox.Checked = this.showPageFreeSpaceToolStripMenuItem.Checked;

            dlg.ShowDialog();

            if (dlg.DialogResult == DialogResult.OK) {
                try {

                    if (dlg.sqlAuthCheckBox.Checked) {
                        // Use SQL auth constructor
                        dba = new DBAnalyzer(dlg.SQLServerName, dlg.DatabaseName, dlg.UserID, dlg.Pwd);
                    }
                    else {
                        // Use Windows auth constructor
                        dba = new DBAnalyzer(dlg.SQLServerName, dlg.DatabaseName);
                    }

                    // Pull back display preferences and set in the form
                    highlightFragPref = dlg.highlightFragCheckbox.Checked;
                    showPageFreeSpacePref = dlg.showPageFreeSpaceCheckbox.Checked;

                    // Also adjust the menus to reflect current prefs
                    highlightFragToolStripMenuItem.Checked = highlightFragPref;
                    showPageFreeSpaceToolStripMenuItem.Checked = showPageFreeSpacePref;
  
                    runAnalysis();
                }
                catch (Exception e) {

                    MessageBox.Show("Error Analyzing " + dba.DatabaseName + " on " + dba.SQLServerName
                        + "\n" + e.Message
                        //+ "\n" + e.Source 
                        //+ "\n" + e.StackTrace
                        , "Error");

                    Debug.WriteLine("Error Analyzing " + dba.DatabaseName + " on " + dba.SQLServerName
                        + "\n" + e.Message
                        + "\n" + e.Source
                        + "\n" + e.StackTrace
                        );
                }
            }
        }

        private void runAnalysis() {

            // Call runAnalysis in the DBAnalyzer to process the database

            this.Text = "File Layout Viewer (" + dba.DatabaseName + ")";

            // Listen for progress from pa performing the database scan
            dba.ProgressChanged += new ProgressChangedEventHandler(pa_ProgressChanged);

            // Listen for the database scan and analysis to complete
            dba.AnalysisComplete += new EventHandler(dba_AnalysisComplete);

            // Clear prog bar
            tsProgBar.Value = 0;
            tsStatusLabel.Text = "";

            // Clear existing image, if any
            pbm = null;

            // Scan and analyze the database asynchronously
            try {               
                dba.runAnalysis();
            }

            catch (Exception e) {

                MessageBox.Show("Error Analyzing " + dba.DatabaseName + " on " + dba.SQLServerName
                    + "\n" + e.Message
                    , "Error");

                Debug.WriteLine("Error Analyzing " + dba.DatabaseName + " on " + dba.SQLServerName
                    + "\n" + e.Message
                    + "\n" + e.Source
                    + "\n" + e.StackTrace
                    );
            }
        }

        void constructBitmap() {

            // Paint (or repaint) the bitmap image

            pbm = new PageBitmap(
                dba,
                highlightFragPref,
                showPageFreeSpacePref
            );

            // Refresh the main form picture box and magnifier
            // with the new image

            mainPictureBox.Image = pbm.pageAnalysisImage;

            mag = new Magnifier();
            mag.Owner = this;
            mag.pbm = pbm.pageAnalysisImage;

        }

        private void fillPageDetails(PageInfo p) {

            // Populate the text boxes that display page detail
            // if p is in our object list

            DBObjInfo o;
            string tempTypeLabel;

            clearPageDetails();

            // Always show file id / page id
            FileIDText.Text = p.FileID.ToString();
            PageIDText.Text = p.PageID.ToString();

            // Only show other values if page is allocated
            if (p.GAMAllocated || p.SGAMAllocated || p.PageType == 11) {

                if (pageTypeLabels.TryGetValue(p.PageType, out tempTypeLabel)) {
                    PageTypeText.Text = p.PageType.ToString() + " : " + tempTypeLabel;
                }
                else {
                    PageTypeText.Text = p.PageType.ToString();
                }

                PrevFileText.Text = p.PrevFile.ToString();
                PrevPageText.Text = p.PrevPage.ToString();
                NextFileText.Text = p.NextFile.ToString();
                NextPageText.Text = p.NextPage.ToString();

                if (dba.DBObjList.TryGetValue(p.PartitionID, out o)) {
                    // If this page is allocated and we know what object it's for
                    // then populate details
                    SchemaText.Text = o.SchemaName;
                    ObjectText.Text = o.ObjectName;
                    IndexText.Text = o.IndexName;
                    PartitionText.Text = o.PartitionNo.ToString();

                    ObjectIDText.Text = p.ObjectID.ToString();
                    IndexIDText.Text = p.IndexID.ToString();
                    PartitionIDText.Text = p.PartitionID.ToString();

                    if (DBAnalyzer.IsFrag(p)) {
                        FragText.Text = "True";
                    }
                    else {
                        FragText.Text = "False";
                    }

                    FreeSpaceText.Text = p.FreeSpace.ToString() + "%";
                }
            }
        }

        private void clearPageDetails() {
            foreach (TextBox c in pageDetailControls) {
                c.Clear();
            }
        }

        private void updateMagnifierPosition(MouseEventArgs e) {
            // Translate the magnifier form to follow the location of the mouse
            // if the left button is pressed

            // Round the magnifier location y position to a multiple of the bar height 
            // in the bitmap, to make it "snap" to a bar interval

            Point magLocation = new Point(
                e.X,
                ((e.Y / pbm.barHeight) * pbm.barHeight) + (pbm.barHeight / 2)
            );

            // Pass this location over the bitmap to the magnifier to act as
            // the center of the magnified image area. 

            mag.targetLocation = magLocation;

            // Offset the top left corner of the magnifier to center it
            // on the cursor location

            magLocation.Offset(-mag.Width / 2, -mag.Height / 2);

            // Magnifier is not a child of this form so translate location to absolute  
            // screen coordinates to position it on the screen

            mag.Location = mainPictureBox.PointToScreen(magLocation);

        }
        #endregion

        #region Event Handlers

        private void Form1_Shown(object sender, EventArgs e) {
            // As soon as the form opens, show the database picker dialog
            ShowChooseDatabase();
            this.panel1.Focus();
        }

        #region Async process, Progress bar, progress labels event handlers

        delegate void setProgressValueCallback(int value);
        delegate void setStatusLabelTextCallback(string value);

        void pa_ProgressChanged(object sender, ProgressChangedEventArgs e) {
            setProgressValue(e.ProgressPercentage);
            setStatusLabel("Read Pages: " + e.UserState.ToString());
        }

        // These two functions intended to be threadsafe so multiple worker
        // threads can update the status in the status bar while the analysis runs:

        void setProgressValue(int value) {
            if (tsProgBar.Owner.InvokeRequired) {
                setProgressValueCallback d = new setProgressValueCallback(setProgressValue);
                this.Invoke(d, new object[] { value });
            }
            else {
                tsProgBar.Value = value;
            }
        }

        void setStatusLabel(string value) {
            if (tsStatusLabel.Owner.InvokeRequired) {
                setStatusLabelTextCallback d = new setStatusLabelTextCallback(setStatusLabel);
                this.Invoke(d, new object[] { value });
            }
            else {
                tsStatusLabel.Text = value;
            }
        }

        void dba_AnalysisComplete(object sender, EventArgs e) {
            // The analysis of the database should be complete, so we can render an image
            // from the data collected
            constructBitmap();
        }

        #endregion

        #region Main picture box event handlers

        void mainPictureBox_MouseMove(object sender, MouseEventArgs e) {
            // As the mouse is moved over the fileviewer bitmap, update the values
            // in text fields to report database object and page information

            // Compute the database page represented by the 
            // location of the mouse over the bitmap

            if (mainPictureBox.ClientRectangle.Contains(e.Location)) {

                mouseClickLocation = e.Location;

                if (e.Button == System.Windows.Forms.MouseButtons.Left) {
                    updateMagnifierPosition(e);
                    mag.Refresh();
                }

                if (dba != null && pbm != null) {

                    // If we got a coordinate that resolves to a database page
                    // in the pagelist, then populate values describing that page

                    PageInfo? p = getPageFromMousePosition(e.Location);
                    if (p.HasValue) {
                        fillPageDetails((PageInfo)p);
                    }
                    else {
                        clearPageDetails();
                    }           
                }
            }
        }

        private PageInfo? getPageFromMousePosition(Point e) {
            PageInfo? p;

            int row = e.Y / pbm.barHeight;
            int col = e.X / pbm.barWidth;

            int page = (row * (pbm.pageAnalysisImage.Width / pbm.barWidth) ) + col;

            // If we got a coordinate that resolves to a database page
            // in the pagelist, then populate values describing that page

            if (page >= 0 && page < dba.PageList.Count()) {
                p = dba.PageList[page];
            }
            else {
                p = null;
            }
            return p;
        }

        private void mainPictureBox_MouseDown(object sender, MouseEventArgs e) {
            // Show the magnifier when the mouse is down in the bitmap
            // If the user right-clicked, also capture the position for 
            // context menu operations
            mouseClickLocation = e.Location;
            if (mag != null) {
                if (e.Button == MouseButtons.Left) {
                    updateMagnifierPosition(e);
                    mag.Show();
                }
            }
            if (e.Button == MouseButtons.Right) {             
                contextMenuStrip1.Show(mainPictureBox.PointToScreen(e.Location) );
            }
        }

        private void mainPictureBox_MouseLeave(object sender, EventArgs e) {
            clearPageDetails();
            if (mag != null) { mag.Hide(); }
        }

        private void mainPictureBox_MouseUp(object sender, MouseEventArgs e) {
            // Hide the magnifier when the mouse is up in the bitmap
            if (mag != null) { mag.Hide(); }
        }

        #endregion 

        #region Menu event handlers

        // File
        private void exitToolStripMenuItem_Click(object sender, EventArgs e) {
            this.Close();
        }

        private void analyzeDatabaseToolStripMenuItem_Click(object sender, EventArgs e) {
            ShowChooseDatabase();
        }

        // Settings
        private void showPageFreeSpaceToolStripMenuItem_MouseUp(object sender, MouseEventArgs e) {
            showPageFreeSpacePref = showPageFreeSpaceToolStripMenuItem.Checked;
            // If there's a bitmap already, repaint it with new setting
            if (pbm != null) constructBitmap();
        }

        private void highlightFragToolStripMenuItem_MouseUp(object sender, MouseEventArgs e) {
            highlightFragPref = highlightFragToolStripMenuItem.Checked;
            // If there's a bitmap already, repaint it with new setting
            if (pbm != null) constructBitmap();
        }

        // Help
        private void aboutToolStripMenuItem_Click(object sender, EventArgs e) {
            AboutBox1 ab = new AboutBox1();
            ab.Show();
        }

        // Context

        private void dBCCPageToolStripMenuItem_Click(object sender, EventArgs e) {

            DBCCOutputForm f = new DBCCOutputForm();
            PageInfo? p = getPageFromMousePosition(mouseClickLocation);
            if ( p.HasValue ) {
                PageInfo q = (PageInfo)p;
                f.DBCCPageTextBox.Text = dba.getDBCCPAGEOutput(q.FileID, q.PageID);
                f.Text = "DBCC PAGE " + dba.DatabaseName + " ( " + q.FileID + " : " + q.PageID + " )";
                
                f.Show();
            }
        }

        #endregion

        #endregion
    }
}
